package com.googlecode.contests_repo.core.users;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;

import com.googlecode.contests_repo.core.config.ConfigurationManager;
import com.googlecode.contests_repo.core.model.User;

import eu.vahlas.json.schema.JSONSchema;
import eu.vahlas.json.schema.JSONSchemaProvider;
import eu.vahlas.json.schema.impl.JacksonSchemaProvider;

/**
 * Controller with methods related to user management.
 * 
 * @author deni, joro
 */
public class UserManager {

	/**
	 * Saves the given user in a file with the specified name. Please note, that
	 * the password is not saved in plain-text. Instead, its SHA-256 hash code
	 * is saved.
	 * 
	 * @param user
	 *            the user to save
	 * @param filename
	 *            the name of the file where the user should be saved.
	 * 
	 * @throws JsonGenerationException
	 * @throws JsonMappingException
	 * @throws IOException
	 * @throws NoSuchAlgorithmException
	 */
	public static void saveUser(User user, String filename)
			throws JsonGenerationException, JsonMappingException, IOException,
			NoSuchAlgorithmException {
		ObjectMapper mapper = new ObjectMapper();
		String hashedPassword = getHash(user.getPassword());
		user.setPassword(hashedPassword);

		mapper.writeValue(new File(filename), user);
	}

	/**
	 * Saves the given user in the default location
	 * (<users-dir>/<username>/user.json).
	 * 
	 * @param user
	 *            the user to save
	 * @throws JsonGenerationException
	 * @throws JsonMappingException
	 * @throws NoSuchAlgorithmException
	 * @throws IOException
	 */
	public static void saveUser(User user) throws JsonGenerationException,
			JsonMappingException, NoSuchAlgorithmException, IOException {
		String userFolderName = ConfigurationManager.getInstance()
				.getUsersDirectory() + File.separator + user.getLogin();
		File userFolder = new File(userFolderName);
		userFolder.mkdir();
		String filename = userFolder + File.separator + "user.json";
		saveUser(user, filename);
	}

	/**
	 * Gets the SHA-256 hash code of the given password.
	 * 
	 * @param password
	 *            the password whose hash code should be returned.
	 * @return the SHA-256 hash code of the given password
	 */
	public static String getHash(String password) {
		MessageDigest digest;
		try {
			digest = MessageDigest.getInstance("SHA-256");

			digest.reset();
			byte[] bytes = digest.digest(password.getBytes("UTF-8"));

			StringBuffer hexString = new StringBuffer();
			for (int i = 0; i < bytes.length; i++) {
				hexString.append(Integer.toHexString(0xFF & bytes[i]));
			}

			return hexString.toString();
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}

		return null;
	}

	/**
	 * Checks whether the given file contains valid user data. Please note, that
	 * the method expects a file in valid JSON format and uses user-schema.json
	 * for the validation.
	 * 
	 * @param file
	 *            the file to validate
	 * @return a list of all validation errors
	 * @throws IOException
	 */
	public static List<String> validateUserBySchema(File file)
			throws IOException {
		List<String> errors = null;

		// Jackson parsing API: the ObjectMapper can be provided
		// and configured differently depending on the application
		ObjectMapper mapper = new ObjectMapper();

		// Allows to retrieve a JSONSchema object on various sources
		// supported by the ObjectMapper provided
		JSONSchemaProvider schemaProvider = new JacksonSchemaProvider(mapper);

		// Retrieves a JSON Schema object based on a file
		InputStream schemaIS = new FileInputStream("user-schema.json");
		JSONSchema schema = schemaProvider.getSchema(schemaIS);

		// Validates a JSON Instance object stored in a file
		InputStream instanceIS = new FileInputStream(file);
		errors = schema.validate(instanceIS);
		instanceIS.close();
		return errors;
	}

	/**
	 * Checks whether the given file contains valid user data, i.e. the file in
	 * valid JSON format, conforms to user-schema.json. If expectedUsername
	 * parameter is not null, also check the login property in the file is the
	 * same.
	 * 
	 * @param userJsonFile
	 *            the file to validate
	 * @param expectedUsername
	 *            expected username or null
	 * @return a list of all validation errors
	 * @throws JsonMappingException
	 * @throws IOException
	 */
	public static List<String> validateUser(File userJsonFile,
			String expectedUsername) throws JsonMappingException, IOException {
		List<String> errors = new ArrayList<String>();

		// check user.json exists
		if (!userJsonFile.exists()) {
			errors.add("Could not find user.json");
			return errors;
		}

		// check user.json is a valid json file
		// stored in a folder named as the user's login
		ObjectMapper mapper = new ObjectMapper();
		try {
			User user = mapper.readValue(userJsonFile, User.class);
			if (expectedUsername != null
					&& !expectedUsername.equals(user.getLogin())) {
				errors.add("Login was expected to be " + expectedUsername
						+ ", but was " + user.getLogin() + ".");
			}
		} catch (JsonParseException e) {
			errors.add("Syntax error: user.json is not a valid JSON file.");
			return errors;
		}

		// check against user-schema
		List<String> schemaErrors = validateUserBySchema(userJsonFile);
		errors.addAll(schemaErrors);

		return errors;
	}

	/**
	 * Validates all users in the repository with the given root and outputs the
	 * result in the console.
	 * 
	 * @param repoRoot
	 *            the root of the file system of the repository
	 * @throws IOException
	 */
	public static void validateAllUsers(String repoRoot) throws IOException {
		File usersDir = new File(repoRoot + File.separator + "souls"
				+ File.separator + "users");
		if (!usersDir.exists() || !usersDir.isDirectory()) {
			System.out.println("Could not find users directory");
			return;
		}

		File[] userDirs = usersDir.listFiles();
		for (File dir : userDirs) {
			if (dir.isDirectory()) {
				String username = dir.getName();
				System.out.println("Validating " + username + "...");

				List<String> errors = validateUser(new File(dir, "user.json"),
						username);
				if (errors.isEmpty()) {
					System.out.println(username + " is a valid user.");
				} else {
					for (String error : errors) {
						System.out.println(error);
					}
				}

				System.out.println();
			}
		}
	}

	/**
	 * Checks whether a user with the specified login already exists.
	 * 
	 * @param login
	 *            the login of the user
	 * @return true if a user with the given login name already exists, false
	 *         otherwise
	 */
	public static boolean isLoginTaken(String login) {
		File userJsonFile = getUserFile(login);
		return userJsonFile.exists();
	}

	private static File getUserFile(String username) {
		String filename = ConfigurationManager.getInstance()
				.getUsersDirectory()
				+ File.separator
				+ username
				+ File.separator + "user.json";
		File userJsonFile = new File(filename);
		return userJsonFile;
	}

	/**
	 * Checks authorization credentials of the user.
	 * 
	 * @param username
	 * @param password
	 * @return true if the credentials are valid, false otherwise
	 */
	public static boolean isAuthorizedUser(String username, String password) {
		if (isInitialized(password) || isInitialized(username)) {
			return false;
		}

		File userFile = getUserFile(username);
		if (!userFile.exists()) {
			return false;
		}

		User user = null;
		ObjectMapper mapper = new ObjectMapper();
		try {
			user = mapper.readValue(userFile, User.class);
		} catch (JsonParseException e) {
			e.printStackTrace();
		} catch (JsonMappingException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

		if (user == null) {
			return false;
		}

		String hashedPassword = getHash(password);
		return hashedPassword.equals(user.getPassword());
	}

	private static boolean isInitialized(String propertyValue) {
		return propertyValue == null || propertyValue.isEmpty();
	}

	/**
	 * Checks whether the given user has the specified right on the given
	 * resource. Takes into account that access rights can be specified not only
	 * for the resource itself, but also for some of its ancestors. Besides,
	 * they are recursive - for example, VIEW_FULL includes VIEW, which in turn
	 * includes LIST.
	 * 
	 * @param username
	 *            the user for whom access rights should be checked
	 * @param resourcePath
	 *            absolute path to the resource for which access rights should
	 *            be checked
	 * 
	 * @param right
	 *            the desired right
	 * @param repoRoot
	 *            absolute path to the root of the repository file system
	 * @return true if the user has the specified right on the given resource;
	 *         false otherwise
	 * 
	 * @throws JsonParseException
	 * @throws JsonMappingException
	 * @throws IOException
	 */
	@SuppressWarnings("unchecked")
	public static boolean hasAccessRight(String username, String resourcePath,
			AccessRight right, String repoRoot) throws JsonParseException,
			JsonMappingException, IOException {

		if (AccessRight.NONE.equals(right)) {
			return true;
		}

		assert resourcePath.startsWith(repoRoot) : "The specified file is not in the repository!";
		String securityFileName = ConfigurationManager.getInstance()
				.getSecurityFileName();

		String currentDir = repoRoot;
		String relativePath = resourcePath.substring(repoRoot.length() + 1);
		String escapedSeparator = Pattern.quote(File.separator);
		for (String part : relativePath.split(escapedSeparator)) {
			String currentRightName = null;

			currentDir += File.separator + part;
			File securityFile = new File(currentDir, securityFileName);
			if (!securityFile.exists()) {
				continue;
			}
			
			if (securityFile.exists()) {
				ObjectMapper mapper = new ObjectMapper();
				Map<String, Object> securityData = mapper.readValue(
						securityFile, Map.class);
				Map<String, Object> grants = (Map<String, Object>) securityData
						.get("grants");
				currentRightName = (String) grants.get(username);
			}
			if (currentRightName != null) {
				AccessRight currentRight = Enum.valueOf(AccessRight.class,
						currentRightName.toUpperCase());
				if (currentRight.includes(right)) {
					// cannot apply LIST recursively
					if (AccessRight.LIST.equals(currentRight)
							&& !currentDir.equals(resourcePath)) {
						continue;
					}

					return true;
				}
			}
		}

		return false;
	}

	/**
	 * Checks whether the given user has the specified right on the given
	 * resource. Takes into account that access rights can be specified not only
	 * for the resource itself, but also for some of its ancestors. Besides,
	 * they are recursive - for example, VIEW_FULL includes VIEW, which in turn
	 * includes LIST.
	 * 
	 * @param username
	 *            the user for whom access rights should be checked
	 * @param resourcePath
	 *            absolute path to the resource for which access rights should
	 *            be checked
	 * 
	 * @param right
	 *            the desired right
	 * @return true if the user has the specified right on the given resource;
	 *         false otherwise
	 * 
	 * @throws JsonParseException
	 * @throws JsonMappingException
	 * @throws IOException
	 */
	public static boolean hasAccessRight(String username, String resourcePath,
			AccessRight right) throws JsonParseException, JsonMappingException,
			IOException {
		String repoRoot = ConfigurationManager.getInstance().getProperty(
				"repoRoot");
		return hasAccessRight(username, resourcePath, right, repoRoot);
	}
}
